/*
 * Copyright (C) 2009 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.droidlogic.inputmethod.remote;

import com.droidlogic.inputmethod.remote.RemoteIME.DecodingInfo;

import java.util.Vector;

import android.content.Context;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.RectF;
import android.graphics.Paint.FontMetricsInt;
import android.graphics.drawable.Drawable;
import android.os.Handler;
import android.util.AttributeSet;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.View;

/**
 * View to show candidate list. There two candidate view instances which are
 * used to show animation when user navigates between pages.
 */
public class CandidateView extends View {
        /**
         * The minimum width to show a item.
         */
        private static final float MIN_ITEM_WIDTH = 22;

        /**
         * Suspension points used to display long items.
         */
        private static final String SUSPENSION_POINTS = "...";

        /**
         * The width to draw candidates.
         */
        private int mContentWidth;

        /**
         * The height to draw candidate content.
         */
        private int mContentHeight;

        /**
         * Whether footnotes are displayed. Footnote is shown when hardware keyboard
         * is available.
         */
        private boolean mShowFootnote = true;

        /**
         * Balloon hint for candidate press/release.
         */
        private BalloonHint mBalloonHint;

        /**
         * Desired position of the balloon to the input view.
         */
        private int mHintPositionToInputView[] = new int[2];

        /**
         * Decoding result to show.
         */
        private DecodingInfo mDecInfo;

        /**
         * Listener used to notify IME that user clicks a candidate, or navigate
         * between them.
         */
        private CandidateViewListener mCvListener;

        /**
         * Used to notify the container to update the status of forward/backward
         * arrows.
         */
        private ArrowUpdater mArrowUpdater;

        /**
         * If true, update the arrow status when drawing candidates.
         */
        private boolean mUpdateArrowStatusWhenDraw = false;

        /**
         * Page number of the page displayed in this view.
         */
        private int mPageNo;

        /**
         * Active candidate position in this page.
         */
        private int mActiveCandInPage;

        /**
         * Used to decided whether the active candidate should be highlighted or
         * not. If user changes focus to composing view (The view to show Pinyin
         * string), the highlight in candidate view should be removed.
         */
        private boolean mEnableActiveHighlight = true;

        /**
         * The page which is just calculated.
         */
        private int mPageNoCalculated = -1;

        /**
         * The Drawable used to display as the background of the high-lighted item.
         */
        private Drawable mActiveCellDrawable;

        /**
         * The Drawable used to display as separators between candidates.
         */
        private Drawable mSeparatorDrawable;

        /**
         * Color to draw normal candidates generated by IME.
         */
        private int mImeCandidateColor;

        /**
         * Color to draw normal candidates Recommended by application.
         */
        private int mRecommendedCandidateColor;

        /**
         * Color to draw the normal(not highlighted) candidates, it can be one of
         * {@link #mImeCandidateColor} or {@link #mRecommendedCandidateColor}.
         */
        private int mNormalCandidateColor;

        /**
         * Color to draw the active(highlighted) candidates, including candidates
         * from IME and candidates from application.
         */
        private int mActiveCandidateColor;

        /**
         * Text size to draw candidates generated by IME.
         */
        private int mImeCandidateTextSize;

        /**
         * Text size to draw candidates recommended by application.
         */
        private int mRecommendedCandidateTextSize;

        /**
         * The current text size to draw candidates. It can be one of
         * {@link #mImeCandidateTextSize} or {@link #mRecommendedCandidateTextSize}.
         */
        private int mCandidateTextSize;

        /**
         * Paint used to draw candidates.
         */
        private Paint mCandidatesPaint;

        /**
         * Used to draw footnote.
         */
        private Paint mFootnotePaint;

        /**
         * The width to show suspension points.
         */
        private float mSuspensionPointsWidth;

        /**
         * Rectangle used to draw the active candidate.
         */
        private RectF mActiveCellRect;

        /**
         * Left and right margins for a candidate. It is specified in xml, and is
         * the minimum margin for a candidate. The actual gap between two candidates
         * is 2 * {@link #mCandidateMargin} + {@link #mSeparatorDrawable}.
         * getIntrinsicWidth(). Because length of candidate is not fixed, there can
         * be some extra space after the last candidate in the current page. In
         * order to achieve best look-and-feel, this extra space will be divided and
         * allocated to each candidates.
         */
        private float mCandidateMargin;

        /**
         * Left and right extra margins for a candidate.
         */
        private float mCandidateMarginExtra;

        /**
         * Rectangles for the candidates in this page.
         **/
        private Vector<RectF> mCandRects;

        /**
         * FontMetricsInt used to measure the size of candidates.
         */
        private FontMetricsInt mFmiCandidates;

        /**
         * FontMetricsInt used to measure the size of footnotes.
         */
        private FontMetricsInt mFmiFootnote;

        private PressTimer mTimer = new PressTimer();

        private GestureDetector mGestureDetector;

        private int mLocationTmp[] = new int[2];

        public CandidateView ( Context context, AttributeSet attrs ) {
            super ( context, attrs );
            Resources r = context.getResources();
            Configuration conf = r.getConfiguration();
            if ( conf.keyboard == Configuration.KEYBOARD_NOKEYS
                    || conf.hardKeyboardHidden == Configuration.HARDKEYBOARDHIDDEN_YES ) {
                mShowFootnote = false;
            }
            mActiveCellDrawable = r.getDrawable ( R.drawable.candidate_hl_bg );
            mSeparatorDrawable = r.getDrawable ( R.drawable.candidates_vertical_line );
            mCandidateMargin = r.getDimension ( R.dimen.candidate_margin_left_right );
            mImeCandidateColor = r.getColor ( R.color.candidate_color );
            mRecommendedCandidateColor = r.getColor ( R.color.recommended_candidate_color );
            mNormalCandidateColor = mImeCandidateColor;
            mActiveCandidateColor = r.getColor ( R.color.active_candidate_color );
            mCandidatesPaint = new Paint();
            mCandidatesPaint.setAntiAlias ( true );
            mFootnotePaint = new Paint();
            mFootnotePaint.setAntiAlias ( true );
            mFootnotePaint.setColor ( r.getColor ( R.color.footnote_color ) );
            mActiveCellRect = new RectF();
            mCandRects = new Vector<RectF>();
        }

        @Override
        protected void onMeasure ( int widthMeasureSpec, int heightMeasureSpec ) {
            int mOldWidth = getMeasuredWidth();
            int mOldHeight = getMeasuredHeight();
            setMeasuredDimension ( getDefaultSize ( getSuggestedMinimumWidth(),
                                                    widthMeasureSpec ), getDefaultSize ( getSuggestedMinimumHeight(),
                                                            heightMeasureSpec ) );
            if ( mOldWidth != getMeasuredWidth() || mOldHeight != getMeasuredHeight() ) {
                onSizeChanged();
            }
        }

        public void initialize ( ArrowUpdater arrowUpdater, BalloonHint balloonHint,
                                 GestureDetector gestureDetector, CandidateViewListener cvListener ) {
            mArrowUpdater = arrowUpdater;
            mBalloonHint = balloonHint;
            mGestureDetector = gestureDetector;
            mCvListener = cvListener;
        }

        public void setDecodingInfo ( DecodingInfo decInfo ) {
            if ( null == decInfo ) { return; }
            mDecInfo = decInfo;
            mPageNoCalculated = -1;
            if ( mDecInfo.candidatesFromApp() ) {
                mNormalCandidateColor = mRecommendedCandidateColor;
                mCandidateTextSize = mRecommendedCandidateTextSize;
            } else {
                mNormalCandidateColor = mImeCandidateColor;
                mCandidateTextSize = mImeCandidateTextSize;
            }
            if ( mCandidatesPaint.getTextSize() != mCandidateTextSize ) {
                mCandidatesPaint.setTextSize ( mCandidateTextSize );
                mFmiCandidates = mCandidatesPaint.getFontMetricsInt();
                mSuspensionPointsWidth =
                    mCandidatesPaint.measureText ( SUSPENSION_POINTS );
            }
            // Remove any pending timer for the previous list.
            mTimer.removeTimer();
        }

        public int getActiveCandiatePosInPage() {
            return mActiveCandInPage;
        }

        public int getActiveCandiatePosGlobal() {
            return mDecInfo.mPageStart.get ( mPageNo ) + mActiveCandInPage;
        }

        /**
         * Show a page in the decoding result set previously.
         *
         * @param pageNo Which page to show.
         * @param activeCandInPage Which candidate should be set as active item.
         * @param enableActiveHighlight When false, active item will not be
         *        highlighted.
         */
        public void showPage ( int pageNo, int activeCandInPage,
                               boolean enableActiveHighlight ) {
            if ( null == mDecInfo ) { return; }
            mPageNo = pageNo;
            mActiveCandInPage = activeCandInPage;
            if ( mEnableActiveHighlight != enableActiveHighlight ) {
                mEnableActiveHighlight = enableActiveHighlight;
            }
            if ( !calculatePage ( mPageNo ) ) {
                mUpdateArrowStatusWhenDraw = true;
            } else {
                mUpdateArrowStatusWhenDraw = false;
            }
            invalidate();
        }

        public void enableActiveHighlight ( boolean enableActiveHighlight ) {
            if ( enableActiveHighlight == mEnableActiveHighlight ) { return; }
            mEnableActiveHighlight = enableActiveHighlight;
            invalidate();
        }

        public boolean activeCursorForward() {
            if ( !mDecInfo.pageReady ( mPageNo ) ) { return false; }
            int pageSize = mDecInfo.mPageStart.get ( mPageNo + 1 )
                           - mDecInfo.mPageStart.get ( mPageNo );
            if ( mActiveCandInPage + 1 < pageSize ) {
                showPage ( mPageNo, mActiveCandInPage + 1, true );
                return true;
            }
            return false;
        }

        public boolean activeCurseBackward() {
            if ( mActiveCandInPage > 0 ) {
                showPage ( mPageNo, mActiveCandInPage - 1, true );
                return true;
            }
            return false;
        }

        private void onSizeChanged() {
            mContentWidth = getMeasuredWidth() - mPaddingLeft - mPaddingRight;
            mContentHeight = ( int ) ( ( getMeasuredHeight() - mPaddingTop - mPaddingBottom ) * 0.95f );
            /**
             * How to decide the font size if the height for display is given?
             * Now it is implemented in a stupid way.
             */
            int textSize = 1;
            mCandidatesPaint.setTextSize ( textSize );
            mFmiCandidates = mCandidatesPaint.getFontMetricsInt();
            while ( mFmiCandidates.bottom - mFmiCandidates.top < mContentHeight ) {
                textSize++;
                mCandidatesPaint.setTextSize ( textSize );
                mFmiCandidates = mCandidatesPaint.getFontMetricsInt();
            }
            mImeCandidateTextSize = textSize;
            mRecommendedCandidateTextSize = textSize * 3 / 4;
            if ( null == mDecInfo ) {
                mCandidateTextSize = mImeCandidateTextSize;
                mCandidatesPaint.setTextSize ( mCandidateTextSize );
                mFmiCandidates = mCandidatesPaint.getFontMetricsInt();
                mSuspensionPointsWidth =
                    mCandidatesPaint.measureText ( SUSPENSION_POINTS );
            } else {
                // Reset the decoding information to update members for painting.
                setDecodingInfo ( mDecInfo );
            }
            textSize = 1;
            mFootnotePaint.setTextSize ( textSize );
            mFmiFootnote = mFootnotePaint.getFontMetricsInt();
            while ( mFmiFootnote.bottom - mFmiFootnote.top < mContentHeight / 2 ) {
                textSize++;
                mFootnotePaint.setTextSize ( textSize );
                mFmiFootnote = mFootnotePaint.getFontMetricsInt();
            }
            textSize--;
            mFootnotePaint.setTextSize ( textSize );
            mFmiFootnote = mFootnotePaint.getFontMetricsInt();
            // When the size is changed, the first page will be displayed.
            //      mPageNo = 0;
            //      mActiveCandInPage = 0;
        }

        private boolean calculatePage ( int pageNo ) {
            if ( pageNo == mPageNoCalculated ) { return true; }
            mContentWidth = getMeasuredWidth() - mPaddingLeft - mPaddingRight;
            mContentHeight = ( int ) ( ( getMeasuredHeight() - mPaddingTop - mPaddingBottom ) * 0.95f );
            if ( mContentWidth <= 0 || mContentHeight <= 0 ) { return false; }
            int candSize = mDecInfo.mCandidatesList.size();
            // If the size of page exists, only calculate the extra margin.
            boolean onlyExtraMargin = false;
            int fromPage = mDecInfo.mPageStart.size() - 1;
            if ( mDecInfo.mPageStart.size() > pageNo + 1 ) {
                onlyExtraMargin = true;
                fromPage = pageNo;
            }
            // If the previous pages have no information, calculate them first.
            for ( int p = fromPage; p <= pageNo; p++ ) {
                int pStart = mDecInfo.mPageStart.get ( p );
                int pSize = 0;
                int charNum = 0;
                float lastItemWidth = 0;
                float xPos;
                xPos = 0;
                xPos += mSeparatorDrawable.getIntrinsicWidth();
                while ( xPos < mContentWidth && pStart + pSize < candSize ) {
                    int itemPos = pStart + pSize;
                    String itemStr = mDecInfo.mCandidatesList.get ( itemPos );
                    float itemWidth = mCandidatesPaint.measureText ( itemStr );
                    if ( itemWidth < MIN_ITEM_WIDTH ) { itemWidth = MIN_ITEM_WIDTH; }
                    itemWidth += mCandidateMargin * 2;
                    itemWidth += mSeparatorDrawable.getIntrinsicWidth();
                    if ( xPos + itemWidth < mContentWidth || 0 == pSize ) {
                        xPos += itemWidth;
                        lastItemWidth = itemWidth;
                        pSize++;
                        charNum += itemStr.length();
                    } else {
                        break;
                    }
                }
                if ( !onlyExtraMargin ) {
                    mDecInfo.mPageStart.add ( pStart + pSize );
                    mDecInfo.mCnToPage.add ( mDecInfo.mCnToPage.get ( p ) + charNum );
                }
                float marginExtra = ( mContentWidth - xPos ) / pSize / 2;
                if ( mContentWidth - xPos > lastItemWidth ) {
                    // Must be the last page, because if there are more items,
                    // the next item's width must be less than lastItemWidth.
                    // In this case, if the last margin is less than the current
                    // one, the last margin can be used, so that the
                    // look-and-feeling will be the same as the previous page.
                    if ( mCandidateMarginExtra <= marginExtra ) {
                        marginExtra = mCandidateMarginExtra;
                    }
                } else if ( pSize == 1 ) {
                    marginExtra = 0;
                }
                mCandidateMarginExtra = marginExtra;
            }
            mPageNoCalculated = pageNo;
            return true;
        }

        @Override
        protected void onDraw ( Canvas canvas ) {
            super.onDraw ( canvas );
            // The invisible candidate view(the one which is not in foreground) can
            // also be called to drawn, but its decoding result and candidate list
            // may be empty.
            if ( null == mDecInfo || mDecInfo.isCandidatesListEmpty() ) { return; }
            // Calculate page. If the paging information is ready, the function will
            // return at once.
            calculatePage ( mPageNo );
            int pStart = mDecInfo.mPageStart.get ( mPageNo );
            int pSize = mDecInfo.mPageStart.get ( mPageNo + 1 ) - pStart;
            float candMargin = mCandidateMargin + mCandidateMarginExtra;
            if ( mActiveCandInPage > pSize - 1 ) {
                mActiveCandInPage = pSize - 1;
            }
            mCandRects.removeAllElements();
            float xPos = mPaddingLeft;
            int yPos = ( getMeasuredHeight() -
                         ( mFmiCandidates.bottom - mFmiCandidates.top ) ) / 2
                       - mFmiCandidates.top;
            xPos += drawVerticalSeparator ( canvas, xPos );
            for ( int i = 0; i < pSize; i++ ) {
                float footnoteSize = 0;
                String footnote = null;
                if ( mShowFootnote ) {
                    footnote = Integer.toString ( i + 1 );
                    footnoteSize = mFootnotePaint.measureText ( footnote );
                    assert ( footnoteSize < candMargin );
                }
                String cand = mDecInfo.mCandidatesList.get ( pStart + i );
                float candidateWidth = mCandidatesPaint.measureText ( cand );
                float centerOffset = 0;
                if ( candidateWidth < MIN_ITEM_WIDTH ) {
                    centerOffset = ( MIN_ITEM_WIDTH - candidateWidth ) / 2;
                    candidateWidth = MIN_ITEM_WIDTH;
                }
                float itemTotalWidth = candidateWidth + 2 * candMargin;
                if ( mActiveCandInPage == i && mEnableActiveHighlight ) {
                    mActiveCellRect.set ( xPos, mPaddingTop + 1, xPos
                                          + itemTotalWidth, getHeight() - mPaddingBottom - 1 );
                    mActiveCellDrawable.setBounds ( ( int ) mActiveCellRect.left,
                                                    ( int ) mActiveCellRect.top, ( int ) mActiveCellRect.right,
                                                    ( int ) mActiveCellRect.bottom );
                    mActiveCellDrawable.draw ( canvas );
                }
                if ( mCandRects.size() < pSize ) { mCandRects.add ( new RectF() ); }
                mCandRects.elementAt ( i ).set ( xPos - 1, yPos + mFmiCandidates.top,
                                                 xPos + itemTotalWidth + 1, yPos + mFmiCandidates.bottom );
                // Draw footnote
                if ( mShowFootnote ) {
                    canvas.drawText ( footnote, xPos + ( candMargin - footnoteSize )
                                      / 2, yPos, mFootnotePaint );
                }
                // Left margin
                xPos += candMargin;
                if ( candidateWidth > mContentWidth - xPos - centerOffset ) {
                    cand = getLimitedCandidateForDrawing ( cand,
                                                           mContentWidth - xPos - centerOffset );
                }
                if ( mActiveCandInPage == i && mEnableActiveHighlight ) {
                    mCandidatesPaint.setColor ( mActiveCandidateColor );
                } else {
                    mCandidatesPaint.setColor ( mNormalCandidateColor );
                }
                canvas.drawText ( cand, xPos + centerOffset, yPos,
                                  mCandidatesPaint );
                // Candidate and right margin
                xPos += candidateWidth + candMargin;
                // Draw the separator between candidates.
                xPos += drawVerticalSeparator ( canvas, xPos );
            }
            // Update the arrow status of the container.
            if ( null != mArrowUpdater && mUpdateArrowStatusWhenDraw ) {
                mArrowUpdater.updateArrowStatus();
                mUpdateArrowStatusWhenDraw = false;
            }
        }

        private String getLimitedCandidateForDrawing ( String rawCandidate,
                float widthToDraw ) {
            int subLen = rawCandidate.length();
            if ( subLen <= 1 ) { return rawCandidate; }
            do {
                subLen--;
                float width = mCandidatesPaint.measureText ( rawCandidate, 0, subLen );
                if ( width + mSuspensionPointsWidth <= widthToDraw || 1 >= subLen ) {
                    return rawCandidate.substring ( 0, subLen ) +
                           SUSPENSION_POINTS;
                }
            } while ( true );
        }

        private float drawVerticalSeparator ( Canvas canvas, float xPos ) {
            mSeparatorDrawable.setBounds ( ( int ) xPos, mPaddingTop, ( int ) xPos
                                           + mSeparatorDrawable.getIntrinsicWidth(), getMeasuredHeight()
                                           - mPaddingBottom );
            mSeparatorDrawable.draw ( canvas );
            return mSeparatorDrawable.getIntrinsicWidth();
        }

        private int mapToItemInPage ( int x, int y ) {
            // mCandRects.size() == 0 happens when the page is set, but
            // touch events occur before onDraw(). It usually happens with
            // monkey test.
            if ( !mDecInfo.pageReady ( mPageNo ) || mPageNoCalculated != mPageNo
                    || mCandRects.size() == 0 ) {
                return -1;
            }
            int pageStart = mDecInfo.mPageStart.get ( mPageNo );
            int pageSize = mDecInfo.mPageStart.get ( mPageNo + 1 ) - pageStart;
            if ( mCandRects.size() < pageSize ) {
                return -1;
            }
            // If not found, try to find the nearest one.
            float nearestDis = Float.MAX_VALUE;
            int nearest = -1;
            for ( int i = 0; i < pageSize; i++ ) {
                RectF r = mCandRects.elementAt ( i );
                if ( r.left < x && r.right > x && r.top < y && r.bottom > y ) {
                    return i;
                }
                float disx = ( r.left + r.right ) / 2 - x;
                float disy = ( r.top + r.bottom ) / 2 - y;
                float dis = disx * disx + disy * disy;
                if ( dis < nearestDis ) {
                    nearestDis = dis;
                    nearest = i;
                }
            }
            return nearest;
        }

        // Because the candidate view under the current focused one may also get
        // touching events. Here we just bypass the event to the container and let
        // it decide which view should handle the event.
        @Override
        public boolean onTouchEvent ( MotionEvent event ) {
            return super.onTouchEvent ( event );
        }

        public boolean onTouchEventReal ( MotionEvent event ) {
            // The page in the background can also be touched.
            if ( null == mDecInfo || !mDecInfo.pageReady ( mPageNo )
                    || mPageNoCalculated != mPageNo ) { return true; }
            int x, y;
            x = ( int ) event.getX();
            y = ( int ) event.getY();
            if ( mGestureDetector.onTouchEvent ( event ) ) {
                mTimer.removeTimer();
                mBalloonHint.delayedDismiss ( 0 );
                return true;
            }
            int clickedItemInPage = -1;
            switch ( event.getAction() ) {
                case MotionEvent.ACTION_UP:
                    clickedItemInPage = mapToItemInPage ( x, y );
                    if ( clickedItemInPage >= 0 ) {
                        invalidate();
                        mCvListener.onClickChoice ( clickedItemInPage
                                                    + mDecInfo.mPageStart.get ( mPageNo ) );
                    }
                    mBalloonHint.delayedDismiss ( BalloonHint.TIME_DELAY_DISMISS );
                    break;
                case MotionEvent.ACTION_DOWN:
                    clickedItemInPage = mapToItemInPage ( x, y );
                    if ( clickedItemInPage >= 0 ) {
                        showBalloon ( clickedItemInPage, true );
                        mTimer.startTimer ( BalloonHint.TIME_DELAY_SHOW, mPageNo,
                                            clickedItemInPage );
                    }
                    break;
                case MotionEvent.ACTION_CANCEL:
                    break;
                case MotionEvent.ACTION_MOVE:
                    clickedItemInPage = mapToItemInPage ( x, y );
                    if ( clickedItemInPage >= 0
                            && ( clickedItemInPage != mTimer.getActiveCandOfPageToShow() || mPageNo != mTimer
                                 .getPageToShow() ) ) {
                        showBalloon ( clickedItemInPage, true );
                        mTimer.startTimer ( BalloonHint.TIME_DELAY_SHOW, mPageNo,
                                            clickedItemInPage );
                    }
            }
            return true;
        }

        private void showBalloon ( int candPos, boolean delayedShow ) {
            mBalloonHint.removeTimer();
            RectF r = mCandRects.elementAt ( candPos );
            int desired_width = ( int ) ( r.right - r.left );
            int desired_height = ( int ) ( r.bottom - r.top );
            mBalloonHint.setBalloonConfig ( mDecInfo.mCandidatesList
                                            .get ( mDecInfo.mPageStart.get ( mPageNo ) + candPos ), 44, true,
                                            mImeCandidateColor, desired_width, desired_height );
            getLocationOnScreen ( mLocationTmp );
            mHintPositionToInputView[0] = mLocationTmp[0]
                                          + ( int ) ( r.left - ( mBalloonHint.getWidth() - desired_width ) / 2 );
            mHintPositionToInputView[1] = -mBalloonHint.getHeight();
            long delay = BalloonHint.TIME_DELAY_SHOW;
            if ( !delayedShow ) { delay = 0; }
            mBalloonHint.dismiss();
            if ( !mBalloonHint.isShowing() ) {
                mBalloonHint.delayedShow ( delay, mHintPositionToInputView );
            } else {
                mBalloonHint.delayedUpdate ( 0, mHintPositionToInputView, -1, -1 );
            }
        }

        private class PressTimer extends Handler implements Runnable {
                private boolean mTimerPending = false;
                private int mPageNoToShow;
                private int mActiveCandOfPage;

                public PressTimer() {
                    super();
                }

                public void startTimer ( long afterMillis, int pageNo, int activeInPage ) {
                    mTimer.removeTimer();
                    postDelayed ( this, afterMillis );
                    mTimerPending = true;
                    mPageNoToShow = pageNo;
                    mActiveCandOfPage = activeInPage;
                }

                public int getPageToShow() {
                    return mPageNoToShow;
                }

                public int getActiveCandOfPageToShow() {
                    return mActiveCandOfPage;
                }

                public boolean removeTimer() {
                    if ( mTimerPending ) {
                        mTimerPending = false;
                        removeCallbacks ( this );
                        return true;
                    }
                    return false;
                }

                public boolean isPending() {
                    return mTimerPending;
                }

                public void run() {
                    if ( mPageNoToShow >= 0 && mActiveCandOfPage >= 0 ) {
                        // Always enable to highlight the clicked one.
                        showPage ( mPageNoToShow, mActiveCandOfPage, true );
                        invalidate();
                    }
                    mTimerPending = false;
                }
        }
}
